JVM레벨에서 발생하는 심각한 문제
애플리케이션 코드로는 복구 불가능
보통 프로그램이 그대로 종료되거나, 프로세스를 재시작해야 함
ex : OutOfMemoryError, StackOverflowError, InternalError
Exception과 차이 정리 : Error는 JVM 내부의 심각한 문제로 개발자가 직접 복구하기 어렵고, Exception은 프로그램 실행 중 발생하는 예외 상황으로 애플리케이션 차원에서 처리 가능
Checked Exception
- 컴파일 시 처리 강제 -> 처리하지 않으면 컴파일 실패
- 외부 환경과 관련된 예외
- 발생하면 컴파일 단계에서 문제를 잡아야 실행 가능
- Checked Exception은 스프링 같은 프레임워크는 대부분 Checked -> Unchcked 변환해서 던지기 때문에 불필요하게 try-catch가 강제되지 않도록 RuntimeException으로 감쌈
- ex :
IOException,SQLException
Unchecked Exception (RuntimeExecption 계열)
- 런타임 시 발생 -> 컴파일러가 체크하지 않음
- 프로그래밍 오류로 인한 예외
- 발생해도 컴파일은 통과, 런타임 시점에서 해당 예외가 발생하면 그 시점 이후 코드 실행 중단
- ex :
NullPointerException,ArithmeticException
try-catch
try { int a = 10 / 0; } catch(ArithmeticException e) { System.out.println("0으로 나눌 수 없습니다."); }
throws
public void readFile() throws IOException { FileReader fr = new FileReader("test.txt"); }
try-with-resources
AutoCloaseable 구현 객체에 적용 -> finally 없이 자동 closeclose() 호출을 try-finally로 변환해서 처리try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) { String line = br.readLine(); } catch (IOException e) { e.printStackTrace(); }
메서드 호출 시 지역변수, 매개 변수, 임시 값이 stack에 저장
예외 발생 시 **stack trace**는 어떤 메서드에서 호출되었는지를 보여줌
stack은 LIFO 구조 -> 가장 최근 호출한 메서드부터 추적
예시
void methodA() { methodB(); } void methodB() { int x = 1 / 0; }
ArithmeticException발생 -> JVM이 stack trace를 통해methodB -> methodA호출 경로를 표시
stack trace
- 예외가 발생했을 때, JVM이 스택에 쌓인 호출 정보를 순서대로 보여주는 것
- 예외 발생 위치(라인), 메서드 호출 순서, 클래스 정보 등이 포함
Stack(스택)
- 메서드 호출 시 생성되는 메모리 영역
- 지역변수, 매개 변수, 임시 값, 객체 주소 저장
- 각 스레드마다 독립적으로 존재
- LIFO 구조 : 마지막 호출한 메서드가 가장 먼저 반환
- 예외 발생 시 stack trace 기록 -> 어디서 예외 발생했는지 추적 가능
- 자동 해제 : 메서드 종료 시 스택 프레임 제거
Heap(힙)
- 동적 메모리 영역
- 객체, 배열, 컬렉션 등 저장
- 여러 스레드가 공유 가능
new키워드로 객체 생성 시 할당- 예외 발생 시에도 힙 메모리는 유지 -> GC 필요
- 참조가 남아있다면 객체는 삭제되지 않고 메모리 누수 가능
Method Area(메소드 영역)
- 클래스 정보, 상수, static 변수, 메서드 코드 등 저장 공간
- JVM이 프로그램 실행 시 클래스 로딩 시점에 생성
- JVM 종료시 자동 제거, 개발자가 직접 삭제 불가
- 내부에 Runtime Constant Pool 존재
- 문자열 리터럴과 상수를 저장하는 공간
- 같은 내용의 문자열이나 상수는 한 번만 저장되고, 여러 곳에서 공유 가능 => 같은 문자열을 반복 생성해도 실제 메모리는 한 개만 쓰고 주소만 공유
Young Generation
- 대부분의 객체가 여기서 생성-> 긍방 사라지는 객체 많음
- GC이후 Eden에서 살아남은 객체를 한 쪽 Survivor로 이동
- 두 영역은 번갈아가며 사용
Old Generation
Permanent Generation / Metaspace
힙에 있는 사용하지 않는 개체 자동 제거
목적 : 메모리 누수 방지, OOM 방지
주요 방식
- Mark & Sweep
- Mark 단계 : 현재 사용중인(참조가 있는) 객체를 표시
- Sweep 단계 : 표시되지 않은(참조되지 않은) 객체 제거
- 장점 : 단순하고 안정적
- 단점 : Heap이 파편화(fragmentation)될 수 있음
- Generational GC
- 객체의 생존 기간에 따라 영역을 나누어 관리
- Young 영역 : 새로 생성된 객체, GC가 자주 일어남
- Old 영역 : 오래 살아남은 객체, GC 발생 빈도 낮음
try-with-resources는 명시적으로 자원을 해제해 참조를 줄여주기 때문에 GC가 불필요하게 객체를 오래 유지하지 않아도 됨
OOM(Out of Memory)
GC 동작
Stop-The-World
Minor GC
Major GC
튜닝포인트
컬렉션에 객체 계속 추가하고 제거 안함
List, Map 같은 컬렉션은 참조를 계속 유지List<byte[]> list = new ArrayList<>(); while(true) { list.add(new byte[1024*1024]); // 계속 객체 생성, 참조 유지 }
Static 변수에 객체 계속 참조
static 변수는 클래스가 로딩되어 있는 동안 메모리에 상주 -> 프로그램 종료 전까지 GC가 회수하지 못함static Map<Integer, String> cache = new HashMap<>(); cache.put(1, "data"); // 계속 참조 유지
Listener / Callback해제 안함
Listener
Callback
GUI, 이벤트 시스템에서 리스너 등록 시 발신자 객체가 수신자 객체를 참조 -> 수신자 객체가 더 이상 필요해도 발신자 객체가 참조를 끊지 않으면 GC 못함
오래 실행되는 프로그램에서 누적 가능
예시
button.addActionListener(myListener); // button이 myListener 참조 // button.dispose() 후에도 myListener 참조 유지
Inner Class/ Anonymous Class
class Outer { class Inner { } } Outer o = new Outer(); Outer.Inner i = o.new Inner(); // i가 참조되는 동안 o도 GC 불가